﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Web.Security;

namespace Evoucher.Business.Models
{
    public interface IVoucherRepository
    {
        IList<Voucher> GetAvailableVouchers(int voucherTypeId, int count);
        Voucher GetVoucherBySerial(string serial);
        VoucherType GetVoucherTypeById(int id);
        List<VoucherType> GetVoucherTypesByProviderId(int providerId);
        List<VoucherType> GetVoucherTypes();
        Provider GetProviderByName(string provider);
        VoucherState GetVoucherStateById(int stateId);
        aspnet_Users GetUserById(Guid uid);
        aspnet_Users GetUserByName(string username);
        TransactionState GetTransactionStateById(int tid);
        Transaction GetTransactionById(long id);
        PaymentGateway GetGatewayInfo(int providerId);
        Transaction InitTransaction(aspnet_Users user, String Email, String Mobile, PaymentGateway transactionGateway, int voucherCount, int voucherTypeId);
        Transaction InitPackageTransaction(aspnet_Users user, PaymentGateway transactionGateway, int packageId);
        Transaction GetTransactionByRefNum(string refrenceNumber);
        List<Transaction> GetTransactions(int pageSize, int pageNumber);
        List<Transaction> GetTransactions(int pageSize, int pageNumber, DateTime? TransactionStartDate,
                                          DateTime? TransactionEndDate, string EmailAddress, string Username, 
                                          int? VoucherTypeId, int? PackageId, int? PaymentGatewayId, 
                                          string voucherSerial);
        int GetCountTransactions();
        List<Package> GetPackages();
        Package GetPackageById(int id);
        List<Package> GetAvailabePackages();
        List<SoldVouchers> GetCountSoldVouchersByDay(DateTime fromDate);
        List<Provider> GetProviders();
        List<PackageItem> GetPackageItemsByPackageId(int id);
        Provider GetProviderById(int id);
        List<PaymentGateway> GetPaymentGateways();
        void AddToVoucherTypes(VoucherType vt);
        void AddToProviders(Provider p);
        void AddToPackages(Package p);
        void AddItemsToPackage(PackageItem p);
        void ImportVoucher(IList<Voucher> vouchers);
        void DeleteVoucher(string pin);
        int GetVouchersCount();
        List<Voucher> GetAvailableVouchers();
        int GetReadyForSaleVouchers();
        int GetSoldVouchersCount();
        void AddTransaction(Transaction t);
        void refresh(object entity);
        void persist();
    }

    public class VoucherRepository : IVoucherRepository
    {
        #region IVoucherRepository Members

        private evoucherEntities repo = new evoucherEntities();

        /// <summary>
        /// This Method returns vouchers which have "ReadyForSale" status or
        /// vouchers which are "Reserved" but they remained more than 30 minutes in reserved state.
        /// that's probably is because of transaction rollbacks or abandoned sessions.
        /// with this trick no voucher will remains unused in "Reserved" state.
        /// </summary>
        /// <param name="voucherTypeId">e.g 3000 tomans irancell, 5000 tomans talya, ...</param>
        /// <param name="count">e.g 1,15,25,50,100</param>
        /// <returns></returns>
        public IList<Voucher> GetAvailableVouchers(int voucherTypeId, int count)
        {
            DateTime thirtyMinutesPast = DateTime.Now.AddMinutes(-30d);

            IQueryable<Voucher> result = from v in repo.Vouchers
                                         where (v.VoucherType.Id == voucherTypeId ) &&
                                               (v.VoucherState.Id == (int)VoucherStates.ReadyForSale || (v.VoucherState.Id == (int)VoucherStates.Reserved && v.LastUpdateDate < thirtyMinutesPast))
                                         select v;

            return result.Take<Voucher>(count).ToList<Voucher>();
        }

        public Voucher GetVoucherBySerial(string serial)
        {
            return repo.Vouchers.FirstOrDefault<Voucher>(v => v.Serial == serial);
        }

        public aspnet_Users GetUserById(Guid uid)
        {
            return repo.aspnet_Users.FirstOrDefault<aspnet_Users>(u => u.UserId == uid);
        }

        public aspnet_Users GetUserByName(string username)
        {
            return repo.aspnet_Users.FirstOrDefault<aspnet_Users>(u => u.UserName == username);
        }

        public TransactionState GetTransactionStateById(int tid)
        {
            return repo.TransactionStates.FirstOrDefault<TransactionState>(ts => ts.Id == tid);
        }

        public PaymentGateway GetGatewayInfo(int providerId)
        {
            IQueryable<PaymentGateway> result = from p in repo.PaymentGateways
                                                where p.Id == providerId
                                                select p;

            return result.FirstOrDefault();
        }

        public Transaction GetTransactionById(long id)
        {
            return repo.Transactions.FirstOrDefault<Transaction>(t => t.Id == id);
        }

        public Transaction GetTransactionByRefNum(string refrenceNumber)
        {
            return repo.Transactions.FirstOrDefault<Transaction>(t => t.ReferenceNumber == refrenceNumber);
        }

        public List<Transaction> GetTransactions(int pageSize, int pageNumber)
        {
            return repo.Transactions.OrderByDescending(t => t.Id).Skip((pageNumber - 1) * pageSize).Take(pageSize).ToList<Transaction>();
        }

        public List<Transaction> GetTransactions(int pageSize, int pageNumber, DateTime? TransactionStartDate,
                                          DateTime? TransactionEndDate, string EmailAddress, string Username,
                                          int? VoucherTypeId, int? PackageId, int? PaymentGatewayId,
                                          string voucherSerial)
        {
            return new List<Transaction>();
        }

        public void AddItemsToPackage(PackageItem p)
        {
            repo.AddToPackageItems(p);
        }

        public void ImportVoucher(IList<Voucher> vouchers)
        {
            IList<string> pins = vouchers.Select(v => v.Pin).ToList<string>();
            IQueryable<Voucher> duplicates = repo.Vouchers.Where(VoucherRepository.BuildContainsExpression<Voucher, string>(e => e.Pin, pins));
               
            if (duplicates.Count() != 0)
            {
                throw new DuplicatePinException(duplicates.ToList<Voucher>()[0].PlainPin);
            }

            foreach (Voucher v in vouchers)
            {
                repo.AddToVouchers(v);
            }
        }

        public void DeleteVoucher(string serial)
        {
            repo.DeleteObject(this.GetVoucherBySerial(serial));
        }

        public VoucherType GetVoucherTypeById(int id)
        {
            IQueryable<VoucherType> result = from vt in repo.VoucherTypes
                                           where vt.Id == id
                                           select vt;

            return result.FirstOrDefault();
        }

        public List<VoucherType> GetVoucherTypesByProviderId(int providerId)
        {
            IQueryable<VoucherType> result = from vt in repo.VoucherTypes
                                             where vt.Provider.Id == providerId
                                             select vt;

            return result.ToList<VoucherType>();
        }

        public List<Voucher> GetAvailableVouchers()
        {
            return repo.Vouchers.Where(v => v.VoucherState.Id != (int)(VoucherStates.Sold)).ToList<Voucher>();
        }

        public Provider GetProviderByName(string provider)
        {
            IQueryable<Provider> result = from p in repo.Providers
                                          where p.Name.ToLower() == provider.ToLower()
                                          select p;

            return result.FirstOrDefault();
        }

        public VoucherState GetVoucherStateById(int stateId)
        {
            IQueryable<VoucherState> result = from v in repo.VoucherStates
                                              where v.Id == stateId
                                              select v;

            return result.FirstOrDefault();
        }

        public Transaction InitTransaction(aspnet_Users user, String Email, String Mobile, PaymentGateway transactionGateway, int voucherCount, int voucherTypeId)
        {
            IList<Voucher> vouchers = this.GetAvailableVouchers(voucherTypeId, voucherCount);

            if (vouchers.Count != voucherCount)
                return null;

            VoucherType vtype = this.GetVoucherTypeById(voucherTypeId);

            Transaction t = new Transaction();
            t.Email = Email;
            t.Mobile = Mobile;
            t.TransactionState = this.GetTransactionStateById((int)TransactionStatus.Init);
            t.TransactionDate = DateTime.Now;
            t.PaymentGateway = transactionGateway;

            if (user != null)
                t.aspnet_Users = user;

            t.Amount = vtype.Price * voucherCount;

            this.AddTransaction(t);

            foreach (Voucher v in vouchers)
            {
                v.VoucherState = this.GetVoucherStateById((int)VoucherStates.Reserved);
                v.LastUpdateDate = DateTime.Now.Date;
                v.Transactions.Add(t);
            }

            return t;
        }

        public int GetCountTransactions()
        {
            return this.repo.Transactions.Count();
        }

        public Transaction InitPackageTransaction(aspnet_Users user, PaymentGateway transactionGateway, int packageId)
        {
            Package p = this.GetPackageById(packageId);
            Transaction t = new Transaction();
            t.aspnet_Users = user;
            user.aspnet_MembershipReference.Load();
            t.Email = user.aspnet_Membership.Email;
            t.Mobile = user.aspnet_Membership.MobilePIN;
            t.TransactionState = this.GetTransactionStateById((int)TransactionStatus.Init);
            t.TransactionDate = DateTime.Now;
            t.PaymentGateway = transactionGateway;
            t.Package = p;
            
            t.Amount = p.Price;
            this.AddTransaction(t);

            p.PackageItems.Load();

            foreach (var item in p.PackageItems)
            {
                item.VoucherTypeReference.Load();
                IList<Voucher> vouchers = this.GetAvailableVouchers(item.VoucherType.Id, item.VoucherCount);
                if (vouchers.Count != item.VoucherCount)
                    return null;

                foreach (Voucher v in vouchers)
                {
                    v.VoucherState = this.GetVoucherStateById((int)VoucherStates.Reserved);
                    v.LastUpdateDate = DateTime.Now.Date;
                    v.Transactions.Add(t);
                }

                VoucherType vtype = item.VoucherType;
            }
            
            return t;
        }

        public List<Package> GetPackages()
        {
            return repo.Packages.ToList<Package>();
        }

        public Package GetPackageById(int id)
        {
            return repo.Packages.FirstOrDefault<Package>( p => p.ID == id );
        }

        public List<Package> GetAvailabePackages()
        {
            DateTime currentDate = DateTime.Now;
            return repo.Packages.Where<Package>( p => p.Active && p.StartDate <= currentDate && p.ExpireDate >= currentDate).ToList<Package>() ;
        }

        public List<SoldVouchers> GetCountSoldVouchersByDay(DateTime fromDate)
        {
            
            var result = from voucher in repo.Vouchers
                         where voucher.LastUpdateDate >= fromDate && voucher.VoucherState.Id == (int)VoucherStates.Sold
                         group voucher by voucher.LastUpdateDate into g
                         orderby g.Key descending
                         select new SoldVouchers { Count = g.Count(), Date = g.Key };

            return result.ToList<SoldVouchers>();
        }

        public List<VoucherType> GetVoucherTypes()
        {
            return repo.VoucherTypes.ToList<VoucherType>();
        }

        public List<Provider> GetProviders()
        {
            return repo.Providers.ToList<Provider>();
        }

        public Provider GetProviderById(int id)
        {
            return repo.Providers.FirstOrDefault<Provider>(p => p.Id == id);
        }

        public List<PackageItem> GetPackageItemsByPackageId(int id)
        {
            return repo.PackageItems.Where(p => p.Package.ID == id).ToList<PackageItem>();
        }

        public List<PaymentGateway> GetPaymentGateways()
        {
            return repo.PaymentGateways.ToList<PaymentGateway>();
        }
        
        public void AddToVoucherTypes(VoucherType vt)
        {
            repo.AddToVoucherTypes(vt);
        }

        public void AddToProviders(Provider p)
        {
            repo.AddToProviders(p);
        }

        public int GetVouchersCount()
        {
            IQueryable<Voucher> result = from v in repo.Vouchers
                                         select v;

            return result.Count();
        }

        public int GetReadyForSaleVouchers()
        {
            IQueryable<Voucher> result = from v in repo.Vouchers
                                         where v.VoucherState.Id == (int)VoucherStates.ReadyForSale
                                         select v;

            return result.Count();
        }

        public int GetSoldVouchersCount()
        {
            IQueryable<Voucher> result = from v in repo.Vouchers
                                         where v.VoucherState.Id == (int)VoucherStates.Sold
                                         select v;

            return result.Count();
        
        }

        public void AddToPackages(Package p)
        {
            repo.AddToPackages(p);
        }

        public void AddTransaction(Transaction t)
        {
            repo.AddToTransactions(t);
        }

        static Expression<Func<TElement, bool>> BuildContainsExpression<TElement, TValue> (Expression<Func<TElement, TValue>> valueSelector, IEnumerable<TValue> values)
        {

            if (null == valueSelector) { throw new ArgumentNullException("valueSelector"); }

            if (null == values) { throw new ArgumentNullException("values"); }

            ParameterExpression p = valueSelector.Parameters.Single();

            // p => valueSelector(p) == values[0] || valueSelector(p) == ...

            if (!values.Any())
            {

                return e => false;

            }

            var equals = values.Select(value => (Expression)Expression.Equal(valueSelector.Body, Expression.Constant(value, typeof(TValue))));

            var body = equals.Aggregate<Expression>((accumulate, equal) => Expression.Or(accumulate, equal));

            return Expression.Lambda<Func<TElement, bool>>(body, p);

        }

        public void persist()
        {
            repo.SaveChanges();
        }

        public void refresh(object entity)
        {
            repo.Refresh(System.Data.Objects.RefreshMode.StoreWins, entity);
        }

        #endregion
    }

    public class DuplicatePinException : Exception
    {
        public String Pin { get; set; }

        public DuplicatePinException(string pin)
            : base("Duplicate Pin Number found!")
        {
            this.Pin = pin;
        }
    }

    public enum VoucherStates
    {
        ReadyForSale = 1,
        Reserved = 2,
        Sold = 3,
        Disabled = 4
    }

    public enum TransactionStatus
    {
        Init = 0,
        ParamSecurityErrors = 10,
        AmountVerifyError = 20,
        Success = 200
    }

    public class SoldVouchers
    {
        public DateTime Date { get; set; }
        public int Count { get; set; }
    }

}
